# -*- encoding: utf-8 -*-
import base64
import time
from enum import Enum
from typing import Optional, Any, List, Union, Callable

import requests

from bct.tools.client.bct4_api import bct_trade_create_api, bct_list_capital_account_api, query_party_url
from bct.tools.common.bct4.base_dto import PagedResult, PagedContainer
from bct.tools.common.bct_util import get_logger
from bct.tools.common.url_builder import url_fmt
from bct.tools.http.auth.authresource import Department, AuthResource, TradeBookResource
from bct.tools.http.calendar.trade_calendar_dto import TradingCalendarDTO
from bct.tools.http.countryparty.party_dto import PartyAccountDTO, CounterPartyDTO
from bct.tools.http.customizeattribute.customize_attribute_dto import CustomizeAttributeDTO
from bct.tools.http.margin.margin_dto import PartyMarginDetail, PartyMargin
from bct.tools.http.pricing.pricing_snapshot_dto import PricingSnapshotDTO
from bct.tools.http.quote.quote_dto import QuoteDTO
from bct.tools.http.trade.bct4_lcm_dto import LcmDTO, LcmEventType
from bct.tools.http.trade.bct4_trade_dto import TradeDTO, FindTrades, FindTradesPagedDTO

logger = get_logger("bct4client")

user_agent = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/97.0.4692.99 Safari/537.36 Edg/97.0.1072.69 '


class JsonConvException(Exception):
    pass


class CallErrorException(Exception):
    pass


class BCTVer(Enum):
    V_4X = "4X"
    V_42 = "42"


def _get_token(auth_username: str, auth_password: str, server_url: str, base64_encode: bool = True) -> str:
    auth_url = "%s%s" % (server_url, '/api/auth-service/users/login')

    if base64_encode:
        usr_name = base64.b64encode(auth_username.encode())
        usr_pwd = base64.b64encode(auth_password.encode())
    else:
        usr_name = auth_username
        usr_pwd = auth_password

    auth_body = {
        'userName': usr_name.decode(),
        'password': usr_pwd.decode()
    }
    headers = {'User-Agent': user_agent}
    auth = requests.post(auth_url, json=auth_body, headers=headers)
    try:
        _token = auth.json()['token']
        return _token
    except Exception:
        error_message = auth.json()['errorMessage']
        raise ValueError(error_message)


def rpc_call(method: str, params: dict, server_url: str, service: Optional[str] = None, token: Optional[str] = None):
    if service is None:
        url = "%s%s" % (server_url, '/api/rpc',)
    else:
        url = "%s/%s%s" % (server_url, service, '/api/rpc',)
    headers = {
        'Authorization': "Bearer %s" % token,
        'User-Agent': user_agent
    }
    body = {'method': method, 'params': params}
    r = requests.post(url, json=body, headers=headers).json()
    if 'error' in r:
        raise JsonConvException(f'failed convertor call {method}')
    return r


def call_post(param: Any, server_url: str, path: Optional[str] = None, token: Optional[str] = None) -> Any:
    url = "%s/%s" % (server_url, path)
    headers = {
        'Authorization': 'Bearer ' + token,
        'User-Agent': user_agent
    }
    body = param
    try:
        r = requests.post(url, json=body, headers=headers)
        if r.status_code == 200:
            if len(r.content) > 0:
                r = r.json()
                if isinstance(r, bool) or isinstance(r, int):
                    return r
                if 'error' in r:
                    raise JsonConvException(f'failed convertor call {path}')
                elif 'errorMessage' in r:
                    raise CallErrorException(r['errorMessage'])
                return r
            else:
                return {}
        else:
            print(r.text)
            return r
    except Exception as e:
        print(e)
        return {}


def call_get(params: dict, server_url: str, path: Optional[str] = None, token: Optional[str] = None):
    url = "%s/%s" % (server_url, path)
    headers = {
        'User-Agent': user_agent,
        'Authorization': 'Bearer ' + token
    }
    r = requests.get(url, headers=headers).json()
    if isinstance(r, bool):
        return r

    if 'error' in r:
        raise JsonConvException(f'failed convertor call {path}')
    else:
        print(r)
    return r


class BCT4Client:
    server_url: str
    script_name: str
    script_password: str
    token: Optional[str] = None
    bct_ver: BCTVer

    def __login(self):
        if self.token is None:
            self.token = _get_token(self.script_name, self.script_password, self.server_url)
            logger.info(
                "login:%s  username:%s bct version:%s" % (self.server_url, self.script_name, self.bct_ver.value))

    def __get_url(self) -> str:
        """
        按照版本号构建url
        :return:
        """
        if self.bct_ver == BCTVer.V_42:
            return "%s%s" % (self.server_url, '/bct')
        else:
            return self.server_url

    def call_post_raw(self, params: dict, path: Optional[str] = None) -> Any:
        """
        发送post请求
        :param params:
        :param path:
        :return:
        """
        self.__login()
        return call_post(params, self.__get_url(), path, self.token)

    def get_param_group_ids(self) -> dict:
        """
        获取定价参数分组
        :return:
        """
        res = self.call_post_raw({}, 'pricing-service/parameter-groups/list')
        return {d['name']: d['id'] for d in res}

    def create_pe(self, pricing_environment_name: str, quote_rules: List, instrument_param_rules: List,
                  trade_param_rules: List,
                  model_rules: List[Any]) -> str:
        params = {
            'name': pricing_environment_name,
            'quoteRules': quote_rules,
            'instrumentParamRules': instrument_param_rules,
            'tradeParamRules': trade_param_rules,
            'modelRules': model_rules
        }
        res = self.call_post_raw(params, 'pricing-service/pricing-environments/upsert')
        return res['id']

    def delete_pricing_environment(self, pricing_environment_name: str) -> bool:
        try:
            pe = self.get_pricing_environment_data(pricing_environment_name)
        except Exception:
            print(f'failed convertor get pe for deletion: {pricing_environment_name}')
            return False
        pe_id = pe['id']
        params = {'id': pe_id}
        res = self.call_post_raw(params, 'pricing-service/pricing-environments/delete')
        if isinstance(res, bool):
            return res
        else:
            print(f'failed convertor delete pe: {pricing_environment_name}')
            return False

    def get_pricing_environment_data(self, pricing_environment_name: str) -> dict:
        params = {'name': pricing_environment_name}
        res = self.call_post_raw(params, 'pricing-service/pricing-environments/list')
        res = [r for r in res if r['name'] == pricing_environment_name]
        if len(res) == 0:
            raise ValueError(f'cannot find pricing environment with name {pricing_environment_name}')
        if len(res) > 1:
            raise ValueError(f'more than one pricing environments found with name {pricing_environment_name}')
        return res[0]

    def calendar_list_all(self) -> List[TradingCalendarDTO]:
        """
        查询交易日历
        注意，不含具体的非交易日
        :return:
        """
        self.__login()
        ret = self.call_post_raw(
            {
            }, 'reference-data-service/calendar/list-all-calendars')
        if isinstance(ret, List):
            calendars = []
            for i in ret:
                calendars.append(TradingCalendarDTO(i))
            return calendars
        return []

    def calendar_upsert(self, calendar: TradingCalendarDTO):
        """
        创建交易日历
        :param calendar:
        :return:
        """
        self.__login()
        all_calendars = self.calendar_list_all()
        for cal in all_calendars:
            if cal.calendar_name == calendar.calendar_name:
                calendar.id = cal.id
                calendar.version = cal.version
        self.call_post_raw(
            calendar.to_dict(), 'reference-data-service/calendar/upsert-calendar')

    def calendar_delete(self, calendar_id: str):
        """
        删除交易日历
        :param calendar_id:
        :return:
        """
        self.__login()
        self.call_post_raw(
            {
                'id': calendar_id

            }, 'reference-data-service/calendar/delete-calendar-by-id')

    def calendar_get(self, calendar_id: str) -> Optional[TradingCalendarDTO]:
        """
        获取交易日历
        :param calendar_id:
        :return:
        """
        self.__login()
        ret = self.call_post_raw(
            {
                'id': calendar_id

            }, 'reference-data-service/calendar/find-calendar-by-id')
        if isinstance(ret, dict):
            return TradingCalendarDTO(ret)
        return None

    def add_pricing_info(self, pricing_snapshot_list: List[PricingSnapshotDTO]):
        """
        添加自定义估值
        :return:
        """
        req = []
        for p in pricing_snapshot_list:
            req.append(p.to_dict())
        ret = call_post(req,
                        self.__get_url(),
                        'margin-service/savePricingResult',
                        self.token)
        logger.info("uploaded")
        return ret

    def calc_trade_and_validate(self, trade: Union[TradeDTO, dict]) -> Any:
        """
        计算和校验交易数据（需要靠bct计算某些数据）
        :param trade:
        :return:
        """
        if isinstance(trade, TradeDTO):
            return call_post(trade.to_dict(), self.__get_url(), 'trade-service/calcTradeAndValidate', self.token)
        else:
            return call_post(trade, self.__get_url(), 'trade-service/calcTradeAndValidate', self.token)

    def trade_create(self, trade: TradeDTO):
        """
        创建交易
        :param trade:
        :return:
        """
        self.__login()
        call_post([trade.to_dict()], self.__get_url(), bct_trade_create_api, self.token)

    def fetch_trades_paged(self, query: FindTradesPagedDTO) -> PagedContainer[Any]:
        """
        分页查询交易
        :param query:
        :param page_num:
        :param page_size:
        :return:
        """
        ret = call_post(
            query.to_dict(),
            self.__get_url(),
            'trade-service/findTradesPaged',
            self.token)

        return PagedContainer(ret)

    def fetch_trade_of_trade_book(self, book_names: List[str], product_types: List[str], page_num: int,
                                  page_size: int) -> Any:
        """
        获取交易簿交易
        :param book_names:
        :param product_types:
        :param page_num:
        :param page_size:
        :return:
        """
        return call_post(
            {
                "page": page_num,
                "pageSize": page_size,
                "bookNames": book_names,
                'productTypes': product_types
            },
            self.__get_url(),
            'trade-service/findTradesPaged',
            self.token)

    def cancel_event(self, event_id: str, trade_id: str, event_type: LcmEventType):
        """

        :param event_id:
        :param trade_id:
        :param event_type:
        :return:
        """
        ret = self.call_post_raw({
            "eventId": event_id,
            "eventType": event_type.name,
            "tradeId": trade_id
        }, "trade-service/cancelEvent")
        print(ret)

    def fetch_trade_events(self, trade_id: str) -> List[LcmDTO]:
        """
        获取交易的生命周期事件表
        :param trade_id:
        :return:
        """
        ret = call_post(
            {
                "tradeId": trade_id
            },
            self.__get_url(),
            'trade-service/findEventsByTradeId',
            self.token)
        result = []
        if isinstance(ret, List):
            for i in ret:
                result.append(LcmDTO(i))
        return result

    def find_trades(self, query: FindTrades) -> List:
        """
        读取交易
        :param query:
        :return:
        """
        json = call_post(
            query.to_dict(),
            self.__get_url(),
            'trade-service/findTrades',
            self.token
        )
        result = []
        if isinstance(json, List):
            for i in json:
                result.append(i)
        return result

    def delete_trade(self, trade_id: str):
        """
        删除交易
        :param trade_id:
        :return:
        """
        call_post(
            {
                "tradeId": trade_id
            },
            self.__get_url(),
            'trade-service/deleteTrade',
            self.token
        )

    def delete_work_flow(self, param: dict) -> Any:
        """
        删除工作流
        :param param:
        :return:
        """
        try:
            return call_post(param,
                             self.__get_url(),
                             "workflow-service/processInstance/deleteProcessInstanceBySubBusinessIds",
                             self.token)
        except Exception as e:
            return e

    def unwind_trade(self, param: dict) -> Any:
        """
        平仓交易
        :param param:
        :return:
        """
        try:
            return call_post(
                param,
                self.__get_url(),
                'trade-service/unwindTrade',
                self.token
            )
        except Exception as e:
            return e

    def fetch_trade_template(self, trade_info, party_id: str) -> Any:
        """
        结算模版
        :return:
        """
        return self.call_post_raw({
            "businessType": trade_info["businessType"],
            "counterpartyIds": [party_id],
            "productTypes": [trade_info['products'][0]['productType']]
        }, 'trade-service/getSimpleSettlementTemplates')

    def instrument_auto_classify(self):
        """
        自动分类标的物
        :return:
        """
        logger.info("开始处理未归类标的")
        call_get({}, self.__get_url(), "market-data-service/exInstrument/instrumentAutoClassify", self.token)
        logger.info("未归类标的处理完成")

    def instrument_list(self, page_num: int, page_size: int) -> PagedResult:
        """
        拉标的物
        :return:
        """
        logger.info("获取标的物")
        result = call_post(
            {
                "page": page_num,
                "pageSize": page_size
            },
            self.__get_url(),
            "market-data-service/exInstrument/listInstruments",
            self.token)
        return PagedResult(result)

    def instrument_save(self, instrument: dict) -> Any:
        """
        保存标的物
        :return:
        """
        logger.info("保存标的物信息")
        return call_post(
            {
                'exInstrumentDto': instrument
            },
            self.__get_url(),
            "market-data-service/exInstrument/saveInstrument",
            self.token)

    def trade_margin_rule_save(self, trade_id: str, margin_config_name: str) -> Any:
        """
        保存交易的保证金信息
        :param trade_id:
        :param margin_config_name:
        :return:
        """
        logger.info("保存交易的保证金规则")
        return call_post(
            {
                'tradeInstanceId': trade_id,
                'marginConfigName': margin_config_name
            },
            self.__get_url(),
            "margin-service/saveMarginConf",
            self.token)

    def instrument_quote_save(self, quote: QuoteDTO) -> Any:
        """
        保存行情
        :param quote:
        :return:
        """
        logger.info("保存行情")
        val = call_post(
            quote.to_dict(),
            self.__get_url(),
            "market-data-service/quote/save",
            self.token
        )
        if 'instrumentId' in val:
            logger.info("%s quote saved" % val['instrumentId'])
        return val

    def find_institution_by_name(self, institution_name: str) -> Optional[dict]:
        """
        按名称查询机构
        :param institution_name:
        :return:
        """
        logger.info("查询机构 %s" % institution_name)
        ret = self.call_post_raw({
            'page': 0,
            'pageSize': 20,
            'orgName': institution_name
        },
            'reference-data-service/institution/page')
        if 'page' in ret and len(ret['page']) > 0:
            return ret['page'][0]
        else:
            return None

    def find_institution_detail_by_id(self, institution_id: str) -> Optional[dict]:
        """
        查询机构详情
        :param institution_id:
        :return:
        """
        logger.info("查询机构详情 id: %s" % institution_id)
        ret = self.call_post_raw({
            'id': institution_id
        },
            'reference-data-service/institution/find-by-id')
        if isinstance(ret, dict):
            return ret
        else:
            return None

    def update_institution_detail(self, institution_info: dict) -> Optional[dict]:
        """
        更新机构信息
        :param institution_info:
        :return:
        """
        logger.info("更新机构信息")
        ret = self.call_post_raw(institution_info,
                                 'reference-data-service/institution/update')
        if isinstance(ret, dict):
            return ret
        else:
            return None

    def find_counter_party_by_code(self, party_code: str) -> Optional[CounterPartyDTO]:
        """
        按照交易对手号查询
        :param party_code:
        :return:
        """
        ret = self.call_post_raw({
            'page': 0,
            'pageSize': 20,
            'partyCode': party_code
        },
            'reference-data-service/party/page-search')
        if 'page' in ret and isinstance(ret['page'], List) and len(ret['page']) > 0:
            a = ret['page'][0]
            dto = CounterPartyDTO()
            dto.party_id = a.get('partyId', '')
            dto.party_code = a.get('partyCode', '')
            dto.party_name = a.get('partyName', '')
            dto.party_status = a.get('partyStatus', '')
            dto.otc_status = a.get('otcStatus', '')
            dto.swap_status = a.get('swapStatus', '')
            return dto
        else:
            return None

    def fetch_country_party(self) -> List[CounterPartyDTO]:
        """
        获取交易对手
        :return:
        """
        self.__login()
        array = call_get({}, self.__get_url(), query_party_url, self.token)
        if isinstance(array, List):
            result = []
            for a in array:
                dto = CounterPartyDTO()
                dto.party_id = a.get('partyId', '')
                dto.party_code = a.get('partyCode', '')
                dto.party_name = a.get('partyName', '')
                dto.party_status = a.get('partyStatus', '')
                dto.otc_status = a.get('otcStatus', '')
                dto.swap_status = a.get('swapStatus', '')
                result.append(dto)
            return result
        else:
            return []

    def capital_account_list(self, party_id: str) -> List[PartyAccountDTO]:
        """
        获取客户的资金账户
        :param party_id:
        :return:
        """
        self.__login()
        ret = call_post({
            'partyId': party_id
        }, self.__get_url(), bct_list_capital_account_api, self.token)
        if isinstance(ret, List):
            result = []
            for acc in ret:
                dto = PartyAccountDTO()
                dto.account_name = acc.get('accountName', '')
                dto.id = acc.get('id', '')
                dto.party_id = acc.get('partyId', '')
                dto.status = acc.get('status', '')
                result.append(dto)
            return result
        else:
            return []

    def fetch_customize_attribute(self) -> List[CustomizeAttributeDTO]:
        """
        自定义字段
        :return:
        """
        self.__login()
        ret = call_post({}, self.__get_url(), 'data-structure-service/definitions/findAllCustomizeAttribute',
                        self.token)
        if isinstance(ret, List):
            result = []
            for attribute in ret:
                dto = CustomizeAttributeDTO(attribute)
                result.append(dto)
            return result
        else:
            return []

    def add_customize_attribute(self, attr: CustomizeAttributeDTO):
        """
        添加自定义字段
        :param attr:
        :return:
        """
        self.__login()
        call_post(attr.to_dict(), self.__get_url(),
                  'data-structure-service/definitions/save',
                  self.token)

    def fetch_dict(self, group_names: List[str], page: int = 0, page_size: int = 20) -> dict:
        """
        拉码表
        :return:
        """
        self.__login()
        return call_post({"page": page, "pageSize": page_size, "groupNames": group_names},
                         self.__get_url(),
                         'reference-data-service/dictionary/page-dict',
                         self.token)

    def find_dict_detail_by_id(self, dict_id: str) -> dict:
        """
        读取码表
        :return:
        """
        self.__login()
        return call_post({"id": dict_id},
                         self.__get_url(),
                         'reference-data-service/dictionary/find-dict-detail-by-id',
                         self.token)

    def update_dict_enum(self, data: dict):
        """
        更新码表
        :return:
        """
        self.__login()
        call_post(data,
                  self.__get_url(),
                  'reference-data-service/dictionary/enum/batch-update-dict-enum',
                  self.token)

    def get_all_instrument(self, data=None):
        """
        查询标的合约
        :return:
        """
        if data is None:
            data = {}
        self.__login()
        return call_post(data,
                         self.__get_url(),
                         '/market-data-service/exInstrument/listInstruments',
                         self.token)

    def auth_all_department_get(self) -> Optional[Department]:
        """
        读取部门列表
        :return:
        """
        self.__login()
        ret = call_post(
            {},
            self.__get_url(),
            'auth-service/authDepartment/authAllDepartmentGet',
            self.token
        )
        if ret:
            return Department(ret)
        return None

    def auth_book_get_can_read(self) -> List[AuthResource]:
        """
        读取交易簿
        :return:
        """
        ret = self.call_post_raw(
            {},
            'auth-service/book/authBookGetCanRead'
        )
        result = []
        if ret:
            for i in ret:
                result.append(AuthResource(i))
        return result

    def auth_non_group_resource_add(self, book: TradeBookResource) -> Any:
        self.__login()
        return self.call_post_raw(
            book.to_dict(),
            'auth-service/authResource/authNonGroupResourceAdd'
        )

    def save_party_margin(self, party_margins: List[PartyMargin]):
        self.__login()
        party_margin_list = []
        for party_margin in party_margins:
            party_margin_list.append(PartyMargin.to_dict(party_margin))
        return self.call_post_raw(
            {"partyMaintenanceMargins": party_margin_list},
            'margin-service/custom/partyMaintenanceMarginSaveBatch',
        )

    def save_party_margin_detail(self, party_margins: List[PartyMarginDetail]):
        self.__login()
        party_margin_list = []
        for party_margin in party_margins:
            party_margin_list.append(PartyMarginDetail.to_dict(party_margin))
        return self.call_post_raw(
            {"partyMaintenanceMarginDetails": party_margin_list},
            'margin-service/custom/partyMaintenanceMarginDetailSaveBatch'
        )

    def fetch_all_counter_party_ignore_status(self) -> List:
        return call_get({}, self.__get_url(), 'reference-data-service/party/list-all?ignoreApprovalStatus=true',
                        self.token)

    def get_counter_party_custom_field(self):
        return self.call_post_raw({"categories": ["PARTY_CUSTOMIZE"]},
                                  'data-structure-service/definitions/findAllByCategory')

    def update_counter_party(self, party_info: dict):
        logger.info('开始修改交易对手: %s' % party_info.get("partyBasicInfoGroup").get("partyName"))
        return self.call_post_raw(party_info,
                                  'reference-data-service/party/updatePartyByNoApproval')

    def find_counter_party_by_id(self, party_id: str) -> Any:
        """
        查询交易对手
        :param party_id:
        :return:
        """
        return self.call_post_raw({"id": party_id},
                                  'reference-data-service/party/find-by-id')

    def __init__(self, server_url: str, script_name: str, script_password: str, bct_ver: BCTVer = BCTVer.V_42):
        """
        初始化客户端
        :param server_url: 服务器地址 http|https://xxxx.xxx.xxx
        :param script_name:  脚本用户名
        :param script_password: 脚本用户密码
        :param bct_ver: 如果是4.1及以下版本，请选择V_4X
        """
        self.bct_ver = bct_ver
        server_url = url_fmt(server_url)
        self.server_url = server_url
        self.script_name = script_name
        self.script_password = script_password
        self.__login()


class Bct4Updater:
    """
    处理一些问题的工具
    交易
    对手
    """
    error_tid: set = set()
    error_msg_dict: dict = {}
    client: Optional[BCT4Client] = None

    def __init__(self, client: BCT4Client):
        self.error_tid = set()
        self.error_msg_dict = {}
        self.client = client

    def print_error(self):
        """
        打印信息
        :return:
        """
        print("========report========")
        if len(self.error_tid) > 0:
            for tid in self.error_tid:
                print('%s 发生错误' % tid)
                err = self.error_msg_dict[tid]
                if err and isinstance(err, dict) and 'errorMessage' in err:
                    print(err['errorMessage'])
                else:
                    print(err)

            file_name = time.strftime('%Y_%m_%d_%M_%I_%S.log', time.localtime())
            with open(file_name, encoding='utf-8', mode='a') as f:
                for tid in self.error_tid:

                    err = self.error_msg_dict[tid]
                    err_msg = ''
                    if err and isinstance(err, dict) and 'errorMessage' in err:
                        err_msg = err['errorMessage']
                    else:
                        err_msg = str(err)
                    f.write('%s 发生错误: %s \n' % (tid, err_msg))
                f.close()

        print('fin')

    def fetch_open_trade(self, trade_id: str) -> Optional[dict]:
        """
        获取开仓时候的交易
        :param trade_id:
        :return:
        """
        open_event = self.fetch_open_event(trade_id)
        if open_event:
            open_query: FindTrades = FindTrades()
            open_query.trade_record_id = open_event.trade_record_id
            result = self.client.find_trades(open_query)
            if result and len(result) > 0:
                return result[0]
        return None

    def fetch_open_event(self, trade_id: str) -> Optional[LcmDTO]:
        """
        获取开仓事件
        :param trade_id:
        :return:
        """
        event_list = self.client.fetch_trade_events(trade_id)
        for event in event_list:
            if event.get_event_type() == LcmEventType.OPEN:
                return event
        print('%s open event not find' % trade_id)
        return None

    def recreate_trade(self, trade_info: dict = None):
        """
        修改交易要素函数
        :param event_id:
        :param trade_info:
        :return:
        """
        trade_id = trade_info['tradeId']

        open_event = self.fetch_open_event(trade_id)

        if open_event is None:
            self.error_tid.add(trade_id)
            self.error_msg_dict[trade_id] = '没有开仓事件'
            return

        trade_info['eventId'] = open_event.event_id
        # patch 365
        if 'annualizedDays' in trade_info and isinstance(trade_info['annualizedDays'], float):
            trade_info['annualizedDays'] = int(trade_info['annualizedDays'])

        # 将status重置
        if 'status' in trade_info:
            trade_info['status'] = None
        # patch需要恢复名义本金
        if 'notional' in trade_info:
            # 对于修改开仓来说，名义本金会被作为期初名义本金，切记！
            trade_info['notional'] = trade_info['initialNotional']
        if 'actualNotional' in trade_info:
            # 这个字段其实没用
            trade_info['actualNotional'] = trade_info['initialNotional']
        if 'products' in trade_info and len(trade_info['products']) > 0:
            # 这个字段其实没用+1
            default_product = trade_info['products'][0]
            default_product['notional'] = default_product['initialNotional']

        ret = self.client.call_post_raw(trade_info, "trade-service/recreateTrade")
        if isinstance(ret, bool) and ret:
            print('%s updated' % trade_info['tradeId'])
            return
        try:
            msg = ret.json()
            print(msg)
        except Exception:
            msg = ret
            print(msg)
        print('%s error' % trade_info['tradeId'])
        self.error_tid.add(trade_info['tradeId'])
        self.error_msg_dict[trade_info['tradeId']] = msg

    def modify_trade(self, trade_id: str, param_in: Any, func: Callable[[dict, any], Union[None, bool]]):
        """
        更新交易
        :param trade_id:
        :param param_in:
        :param func:
        :return:
        """
        open_event = self.fetch_open_event(trade_id)
        open_trade = None
        if open_event:
            open_query: FindTrades = FindTrades()
            open_query.trade_record_id = open_event.trade_record_id
            open_t = self.client.find_trades(open_query)
            if open_t and len(open_t) > 0:
                open_trade = open_t[0]
        else:
            self.error_tid.add(trade_id)
            self.error_msg_dict[trade_id] = "open event not found"
            return

        if open_trade:
            exec_ret = func(open_trade, param_in)
            if exec_ret is False:
                return
            if 'status' in open_trade:
                open_trade['status'] = None
            # 返息记息本金类型
            if 'prePayPremiumAccrualNotionType' in open_trade and (
                    open_trade['prePayPremiumAccrualNotionType'] is None or len(
                open_trade['prePayPremiumAccrualNotionType']) == 0):
                open_trade['prePayPremiumAccrualNotionType'] = 'PRE_PAY_AMOUNT'
            # patch 365
            if 'annualizedDays' in open_trade and isinstance(open_trade['annualizedDays'], float):
                open_trade['annualizedDays'] = int(open_trade['annualizedDays'])
            open_trade['eventId'] = open_event.event_id
            # patch date

            try:
                ret = self.client.call_post_raw(open_trade, "trade-service/recreateTrade")
                if isinstance(ret, bool) and ret:
                    print('%s updated' % open_trade['tradeId'])
                    return
                else:
                    msg = ret.json()
                    print(msg)
            except Exception as e:
                msg = ret
                print(e)
                print(msg)
            print('%s error' % trade_id)
            self.error_tid.add(trade_id)
            self.error_msg_dict[trade_id] = msg
        else:
            print('%s trade not found' % trade_id)
            self.error_tid.add(trade_id)
            self.error_msg_dict[trade_id] = "open snapshot not found"

    def modify_party(self, query_key: str, param_in: Any, func: Callable[[dict, any], Union[None, bool]]):
        """
        更新交易对手
        :param query_key:
        :param param_in:
        :param func:
        :return:
        """
        party = self.client.find_counter_party_by_code(query_key)
        if party:
            party_id = party.party_id
            party_info = self.client.find_counter_party_by_id(party_id)
            try:
                flag_update = func(party_info, param_in)
                if flag_update is None or flag_update is True:
                    # patch
                    # 机构类别
                    if 'institutionBasicInfoGroup' in party_info:
                        ins_base_info = party_info['institutionBasicInfoGroup']
                        if 'orgType' not in ins_base_info or len(ins_base_info['orgType']) == 0:
                            logger.error('%s error: %s' % (query_key, '机构类别为空'))
                            raise Exception("机构类别为空")
                    ret = self.client.update_counter_party(party_info)
                    if isinstance(ret, dict):
                        # 正常返回
                        pass
                    else:
                        response_json = ret.json()
                        if 'errorMessage' in response_json and len(response_json['errorMessage']) > 0:
                            logger.error('%s error: %s' % (query_key, response_json['errorMessage']))
                            raise Exception(response_json['errorMessage'])
                    logger.info('%s updated' % query_key)

            except Exception as e:
                logger.error('%s - %s' % (query_key, str(e)))
                self.error_tid.add(query_key)
                self.error_msg_dict[query_key] = str(e)
        else:
            logger.warning('未找到交易对手 %s' % query_key)
            self.error_tid.add(query_key)
            self.error_msg_dict[query_key] = "未找到交易对手"

    def modify_institution(self, institution_name: str, param_in: Any, func: Callable[[dict, any], Union[None, bool]]):
        """
        机构信息
        :param institution_name:
        :param param_in:
        :param func:
        :return:
        """
        institution = self.client.find_institution_by_name(institution_name)
        if institution:
            institution_id = institution['id']
            institution_info = self.client.find_institution_detail_by_id(institution_id)
            try:
                flag_update = func(institution_info, param_in)
                if flag_update is None or flag_update is True:
                    # patch
                    # 机构类别
                    if 'institutionBasicInfoGroup' in institution_info:
                        ins_base_info = institution_info['institutionBasicInfoGroup']
                        if 'orgType' not in ins_base_info or len(ins_base_info['orgType']) == 0:
                            logger.error('%s error: %s' % (institution_name, '机构类别为空'))
                            raise Exception("机构类别为空")
                    ret = self.client.update_institution_detail(institution_info)
                    if isinstance(ret, dict):
                        # 正常返回
                        pass
                    else:
                        response_json = ret.json()
                        if 'errorMessage' in response_json and len(response_json['errorMessage']) > 0:
                            logger.error('%s error: %s' % (institution_name, response_json['errorMessage']))
                            raise Exception(response_json['errorMessage'])
                    logger.info('%s updated' % institution_name)

            except Exception as e:
                self.error_tid.add(institution_name)
                self.error_msg_dict[institution_name] = str(e)
        else:
            logger.warning('未查询到机构 %s' % institution_name)
            self.error_tid.add(institution_name)
            self.error_msg_dict[institution_name] = "未找到机构"


class CalendarCache:
    """
    日历缓存
    """
    bct4_client: BCT4Client
    cache: dict
    def_cal: TradingCalendarDTO

    def get_calendar(self, calendar_id: str) -> Optional[TradingCalendarDTO]:
        cal = self.cache.get(calendar_id, None)
        if cal is None:
            return self.def_cal
        else:
            return cal

    def init_calcs(self):
        """
        初始化缓存
        :return:
        """
        calendars = self.bct4_client.calendar_list_all()
        for cal in calendars:
            calendar = self.bct4_client.calendar_get(cal.id)
            if calendar:
                self.cache[cal.id] = calendar
            if calendar.calendar_id == 'DEFAULT_CALENDAR':
                self.def_cal = calendar

    def __init__(self, client: BCT4Client):
        self.bct4_client = client
        self.cache = {}
